monoruby JIT コンパイラの Invariant(不変条件)
monoruby の JIT コンパイラは「楽観的な仮定」に基づいて高速なネイティブコードを生成する。仮定が崩れた場合の対処として、deoptimization(インタプリタへのフォールバック) と recompilation(JIT再コンパイル) の2つのメカニズムがある。
1. 型ガード(Type Guard)
実装箇所: codegen/jitgen/guard.rs:82 — guard_class()
インラインキャッシュで観測された型に基づき、演算のオペランドが特定の型であることを仮定する。
code:ruby
def add(a, b)
a + b
end
# 最初の20回は Integer + Integer で呼ばれる → JIT が Fixnum 特化コードを生成
20.times { add(1, 2) }
# 突然 Float を渡す → 型ガード失敗 → deopt
add(1.0, 2.0)
仮定の内容
JIT コンパイル時、インラインキャッシュに (INTEGER_CLASS, INTEGER_CLASS) が記録されていれば、a + b を Fixnum 同士の加算として特化コードを生成する(TraceIR の ic フィールド: trace_ir.rs:150)。
チェックのタイミングと方法
JIT コードの 実行直前(各演算の入口)で、x86-64 命令によりインラインチェック:
code:asm
; Integer guard: ビット0が1ならFixnum
testq rdi, 0b001
jz deopt ; 0なら Fixnum ではない → deopt
; Float guard: ビット0が0かつビット1が1ならFlonum
testq rdi, 0b001
jnz deopt ; Fixnumなら失敗
testq rdi, 0b010
jnz exit ; Flonumならok
; そうでなければRValueヘッダのclass_idをチェック
cmpl rdi + 4, FLOAT_CLASS
jne deopt
; Heap object guard (String, Arrayなど):
testq rdi, 0b111
jnz deopt ; immediateなら失敗
cmpl rdi + 4, expected_class_id
jne deopt
対処
型ガード失敗時は deoptimize: レジスタ状態をスタックに書き戻し(WriteBack)、インタプリタの該当バイトコード位置にジャンプする。
2. BOP(基本演算)ガード
実装箇所: codegen/jitgen/asmir/compile.rs:254 — AsmInst::CheckBOP
定義: globals/store/class.rs:819 — add_basic_op_method()
code:ruby
# Integer#+ はデフォルトの基本演算(BOP)
# JIT は BOP が再定義されていない前提でインライン化
def sum(n)
total = 0
n.times { |i| total += i }
total
end
sum(1000) # JIT が Integer#+ をネイティブ addq 命令に展開
# ここで再定義!
class Integer
def +(other)
self - other # +を−に変えてしまう
end
end
sum(1000) # BOP ガード失敗 → 全JITコード無効化
仮定の内容
Integer#+, Integer#-, Float#* などの基本演算が 元のビルトイン実装のまま であること。
code:BOP として登録されるメソッド一覧
Integer: +, -, *, /, %, <<, >>, &, |, ^, ==, !=, <, <=, >, >=, <=>
実装メカニズム
ビルトインメソッド登録時に is_basic_op: true フラグを設定(class.rs:826)
JIT コードの冒頭で グローバルフラグ bop_redefined_flags を1命令でチェック:
code:asm
cmpl rip + bop_redefined_flags, 0
jnz deopt ; 0でなければ BOP が再定義された
3. メソッド再定義時(insert_method)、上書きされるメソッドに is_basic_op フラグがあれば set_bop_redefine() を呼ぶ(class.rs:1021-1027)
対処(最も激烈)
set_bop_redefine() は 全JITコードを即座に無効化 する(class.rs:1030-1043):
code:rust
fn set_bop_redefine(&mut self) {
CODEGEN.with(|codegen| {
let mut codegen = codegen.borrow_mut();
codegen.set_bop_redefine(); // フラグを立てる
self.invalidate_jit_code(); // 全ISeqのJITエントリを無効化
let vm_entry = codegen.vm_entry();
for func in self.functions.functions() {
if func.is_iseq().is_some() {
// 全関数のエントリポイントをVMエントリに差し替え
let entry = codegen.jit.get_label_address(&func.entry_label());
codegen.jit.apply_jmp_patch_address(entry, &vm_entry);
}
}
});
}
全関数のエントリポイントの jmp 先を VM(インタプリタ)に書き換えることで、以降の呼び出しは全てインタプリタに落ちる。
BOP 再定義の3層への影響
BOP 再定義は 3層 に影響する:
1. JIT コード: 全エントリポイントを VM にパッチ + bop_redefined_flags をセット
2. VM ディスパッチテーブル: 最適化ハンドラを非最適化版に差し替え
3. 今後の JIT: ループ JIT コンパイルも無効化(vm_loop_start_no_opt)
VM ディスパッチテーブルの二重化
VM のディスパッチテーブルには各バイトコード命令に対して 2種類のハンドラ が用意されている(codegen/vmgen.rs):
table:_
状態 + のハンドラ 動作
BOP 未再定義 add_values Fixnum同士なら即座にネイティブ加算、それ以外はメソッド探索
BOP 再定義済 add_values_no_opt 常にフルのメソッド探索を実行
BOP 再定義時に remove_vm_bop_optimization() でディスパッチテーブルを差し替え:
code:rust
// vmgen.rs — BOP再定義後
self.dispatch160 = self.vm_binops(add_values_no_opt);
self.dispatch161 = self.vm_binops(sub_values_no_opt);
self.dispatch162 = self.vm_binops(mul_values_no_opt);
// ループJITの閾値チェックも無効化
self.dispatch14 = self.vm_loop_start_no_opt();
BOP 再定義は 不可逆(one-way switch)であり、一度フラグが立つと元に戻らない。Ruby で Integer#+ を再定義するようなコードは極めて稀なので、この割り切りは妥当である。
Immediate Eviction(即時退避)
BOP 再定義時は、実行中のコールスタック上にある JIT フレームも即座にパッチされる(codegen.rs:1183-1214):
code:rust
fn immediate_eviction(&mut self, mut cfp: Cfp) {
let mut return_addr = unsafe { cfp.return_addr() };
while let Some(prev_cfp) = cfp.prev() {
let ret = return_addr.unwrap();
if !self.check_vm_address(ret) {
// JIT コード内のリターンアドレスを deopt にパッチ
if let Some((patch_point, deopt)) = self.get_deopt_with_return_addr(ret) {
self.jit.apply_jmp_patch_address(patch_point, &deopt);
unsafe { patch_point.as_ptr().write(0xe9) }; // x86-64 jmp
}
}
cfp = prev_cfp;
return_addr = unsafe { cfp.return_addr() };
}
}
コールスタックを遡って JIT フレームの戻りアドレスを deopt コードに差し替えることで、メソッド再定義後に古い JIT コードに戻ることがない ことを保証する。
3. クラスバージョンガード
実装箇所: codegen/jitgen/guard.rs:28 — guard_class_version()
code:ruby
class Dog
def speak; "woof"; end
end
def greet(dog)
dog.speak
end
d = Dog.new
25.times { greet(d) } # JIT: Dog#speak を直接呼ぶコードを生成
# クラスを変更
class Dog
def speak; "bark"; end # メソッド再定義
end
greet(d) # クラスバージョン不一致 → recompile → 新しいspeak を呼ぶ
仮定の内容
メソッド呼び出しサイトでキャッシュした「レシーバクラス → メソッド」の対応が有効であること。
実装メカニズム
グローバルな class_version カウンタ(jit_module.rs:6、i32)
メソッドの追加・削除・可視性変更のたびにインクリメント(class.rs:890, 1006, 1022)
JIT コード内ではキャッシュされたバージョンとグローバルバージョンを比較:
code:asm
movl eax, rip + global_class_version
cmpl eax, rip + cached_class_version
jne version_fail
対処
バージョン不一致時は recompile + deopt:
1. jit_recompile_method を呼び出し、インラインキャッシュを最新の状態で再構築して JIT 再コンパイル
2. その後インタプリタにフォールバック(新しい JIT コードは次回呼び出しから使われる)
特筆すべきは update_inline_cache()(class.rs:1062): 再コンパイル前に、キャッシュされた全メソッドが現在のクラス階層でも同じ関数に解決されるかチェックし、もし異なれば再コンパイルをスキップ(JITコードを破棄)する。
4. 定数バージョンガード
実装箇所: codegen/jitgen/asmir/compile/constants.rs:25 — guard_const_version()
code:ruby
MAX = 100
def check(n)
n < MAX
end
25.times { check(50) } # JIT: MAX=100 を即値として埋め込む
MAX = 200 # 定数再代入 → const_version インクリメント
check(50) # 定数バージョンガード失敗 → deopt
実装メカニズム
グローバルな const_version カウンタ(jit_module.rs:8、i64)
定数の設定・削除時にインクリメント(globals.rs:539, 544)
JIT コードでキャッシュされたバージョンと比較:
code:asm
movq rax, rip + global_const_version
cmpq rax, rip + cached_const_version
jne deopt
対処
deoptimize してインタプリタにフォールバック。次回 JIT コンパイル時に新しい定数値でコード生成。
5. 算術オーバーフローガード
実装箇所: codegen/jitgen/asmir/compile/binary_op.rs:30-53
code:ruby
def factorial(n)
result = 1
(1..n).each { |i| result *= i }
result
end
factorial(20) # Fixnum 範囲内 → JIT のネイティブ乗算で高速
factorial(100) # オーバーフロー → BigNum が必要 → deopt
仮定の内容
Fixnum(i63)同士の四則演算結果が Fixnum に収まること。
チェック方法
x86-64 の オーバーフローフラグ を直接チェック:
code:asm x86-64 の オーバーフローフラグ を直接チェック
; 加算の例
subq lhs, 1 ; タグビットを除去
addq lhs, rhs ; 加算
jo overflow_deopt ; オーバーフローなら deopt
; 乗算の例
sarq rhs, 1 ; タグビット除去
subq lhs, 1
imul lhs, rhs ; 符号付き乗算
jo overflow_deopt
orq lhs, 1 ; タグビット復元
対処
deoptimize → インタプリタが BigNum 演算にフォールバック。deopt 理由は "_arith_overflow" シンボル。
6. キャプチャガード(Capture Guard)
実装箇所: codegen/jitgen/guard.rs:249 — guard_capture()
code:ruby
def make_counter
count = 0
inc = proc { count += 1 }
get = proc { count }
inc, get
end
inc, get = make_counter
inc.call # Proc がローカル変数をキャプチャしている
仮定の内容
JIT コードが実行中のローカルフレームが スタック上にある(ヒープにキャプチャされていない)こと。ブロック/Proc がフレームをキャプチャすると、ローカル変数のアドレスが変わる可能性がある。
対処
deoptimize してインタプリタに戻す。deopt 理由は "__capture_guard"。
7. インラインキャッシュ未充填(NotCached / MethodNotFound)
実装箇所: codegen/jitgen/compile.rs 各所
code:ruby
def dispatch(obj)
obj.foo
end
# 初回コンパイル時にインラインキャッシュがまだ空
# → JIT は RecompileReason::NotCached で即座にインタプリタに戻し
# インタプリタがキャッシュを充填した後に再コンパイル
仮定の内容
JIT コンパイル時にインラインキャッシュに有効なエントリがあること。
対処
JIT コンパイル自体を中断し、CompileResult::Recompile(RecompileReason::NotCached) を返す。一度インタプリタで実行してキャッシュが充填された後、再度 JIT コンパイルが行われる。
Deoptimization の全体フロー
code:_
JIT コード実行中
│
│ ガード失敗 (型不一致, オーバーフロー, バージョン変更, etc.)
▼
WriteBack レジスタ → スタック書き戻し
│ - xmm レジスタの浮動小数点値をスタックスロットに復元
│ - リテラル定数をスタックスロットに復元
│ - r15(アキュムレータ)をスタックスロットに復元
▼
r13 ← 該当バイトコードPC プログラムカウンタ設定
│
▼
jmp vm_fetch VMのfetchループにジャンプ
│
▼
インタプリタが続行(正しい結果を保証)
WriteBack の実装
deoptimization 時にレジスタ状態をスタックに復元する構造体(codegen/jitgen.rs:674-717):
code:rust
pub(crate) struct WriteBack {
xmm: Vec<(Xmm, Vec<SlotId>)>, // xmmレジスタ → スタックスロット
literal: Vec<(Immediate, SlotId)>, // リテラル定数 → スタックスロット
void: Vec<SlotId>, // nil で初期化するスロット
r15: Option<SlotId>, // アキュムレータ → スタックスロット
}
code:rust
pub(super) fn gen_write_back_for_deopt(&mut self, wb: &WriteBack) {
for (xmm, v) in &wb.xmm {
self.xmm_to_stack2(*xmm, v); // xmm → スタック
}
for (v, slot) in &wb.literal {
self.literal_to_stack2(*slot, (*v).into()); // 定数 → スタック
}
for slot in &wb.void {
self.literal_to_stack2(*slot, Value::nil()); // nil → スタック
}
if let Some(slot) = wb.r15 {
movq r14 - offset(slot), r15; // r15 → スタック
}
}
Deopt カウンタによる再コンパイル制御
deoptimize.rs:50-67 の dec_counter は、同じ deopt ポイントで何度も deopt が発生した場合にのみ再コンパイルをトリガーする仕組み:
code:_
deopt 1回目〜9回目 → カウンタをデクリメント、インタプリタにフォールバック
deopt 10回目 → jit_recompile_method() を呼び出して再コンパイル
deopt 10回目以降 → カウンタ < 0 なので直接 deopt(再コンパイルしない)
定数は COUNT_DEOPT_RECOMPILE = 10、特化メソッドは COUNT_DEOPT_RECOMPILE_SPECIALIZED = 50。
RecompileReason(再コンパイル理由)
executor.rs:1759-1764 で定義:
code:rust
pub enum RecompileReason {
NotCached = 0, // インラインキャッシュ未充填
MethodNotFound = 1, // メソッドが見つからない
IvarIdNotFound = 2, // インスタンス変数スロットが見つからない
ClassVersionGuardFailed = 3, // クラスバージョン不一致
}
まとめ
table:_
Invariant チェック方法 変化の契機 対処
型ガード testq/cmpq でビットパターン検査 異なる型の値が渡された deopt
BOP ガード グローバルフラグ1命令チェック Integer#+ 等の再定義 全JIT無効化
クラスバージョン グローバルカウンタ比較 メソッド追加/削除/可視性変更 recompile + deopt
定数バージョン グローバルカウンタ比較 定数の再代入/削除 deopt
算術オーバーフロー CPU の jo フラグ i63 範囲を超える計算 deopt (→ BigNum)
キャプチャガード フレームメタデータ検査 Proc/ブロックがフレームを捕捉 deopt
キャッシュ未充填 コンパイル時チェック 初回コンパイル時 コンパイル中断→再コンパイル